/*  This file is a part of JJSched source code
    JJSched - Java Jabber Scheduler - a simple Jabber scheduling application
    Copyright (C) 2008 Cestmir "CZestmyr" Houska

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/> */

package czestmyr.jjsched;

import java.util.*;
import java.text.*;

/**
 * Handles the manipulation with time differences and time expressions.
 * Also provides some methods for parsing various time expression
 */
public class TimeHandler{
    /**
     * Get actual time, formatted into a string.
     *
     * @return	The string representation of the actual server time
     */
    public String getFormattedTime(){
        cal = Calendar.getInstance();
        return readableTimeFormat.format(cal.getTime());
    }

    /**
     * Get the time specified by the timestamp into a string.
     *
     * @param	timestamp	The timestamp of the time that should be formatted into a string
     * @return 			The string representation of the given timestamp
     */
    public String getFormattedTime(long timestamp){
        return readableTimeFormat.format(timestamp);
    }

    /**
     * Get the time specified by the timestamp in an SQL-compatible string representation
     *
     * @param   timestamp       The timestamp of the time that should be converted to SQL string
     * @return                  The string representation of the timestamp in SQL-compatible format
     */
    public String getSqlFormattedTime(long timestamp){
        return sqlTimeFormat.format(timestamp);
    }

    /**
     * Get the given time offset, formatted in a string
     *
     * @param	timestamp	The timestamp, containing the offset
     * @return			The offset, nicely formatted into a string
     */
    public String getFormattedOffset(long timestamp){
        boolean positive = true;
        if (timestamp < 0) {
            positive = false;
            timestamp = -timestamp;
        }
        //I don't know why this has to be here, but it works :(
        timestamp -= timeDiffValues[2];

        SimpleDateFormat sdf = new SimpleDateFormat("H:mm:ss");
        if (positive)
            return "+ "+sdf.format(timestamp);
        else
            return "- "+sdf.format(timestamp);
    }

    /**
     * Get actual server time in milliseconds
     *
     * @return  The actual server time in milliseconds
     */
    public long getLongTime(){
        cal = Calendar.getInstance();

        return cal.getTimeInMillis();
    }

    /**
     * Parse time, given as a string time expression (see user documentation).
     * The time is in a local time, that differs from the time of the server by timeOffset.
     * This functions thus decreases the returned value by timeOffset.
     *
     * @param	toParse		The string that contains the time expression
     * @param	timeOffset	Difference between the local time and server time (LT - ST)
     * @throws  Exception       In case if parse error
     * @return                  Representation of the time in milliseconds
     */
    public long parseToServerTime(String toParse, long timeOffset) throws Exception{
        long time = 0;

        //Divide toParse string into origin[+plus][-minus]
        String minus = null;
        String plus = null;
        String origin = null;

        int p = toParse.indexOf('+');
        int m = toParse.indexOf('-');
        if (m != -1 && m != toParse.length()-1) {
            minus = toParse.substring(m+1,toParse.length());
        }
        if (p != -1 && p != toParse.length()-1) {
            if (m != -1) {
                if (m > p) {
                    plus = toParse.substring(p+1,m);
                } else
                    throw new ParseException("Relative time specifier error - decreasing should be AFTER increasing", m);
            } else {
                plus = toParse.substring(p+1,toParse.length());
            }
        }
        if (p != -1) {
            origin = toParse.substring(0,p);
        } else if (m != -1) {
            origin = toParse.substring(0,m);
        } else origin = toParse;

        origin = StringParser.beautifyString(origin);

        //Parse the three parts of the time expression
        if(!origin.equals(""))
            time = parseTime(origin, timeOffset);
        else time = parseTime("now", timeOffset);
        time += parseTimeDiff(plus);
        time -= parseTimeDiff(minus);

        //Subtract the time offset
        time -= timeOffset;

        //Return the correct server time
        return time;
    }

    /**
     * Parse the given string as a timediff string as defined in syntax of time specifiers
     *
     * @param	str	The string to be parsed
     * @return		The representation of the timediff string in miliseconds
     * @throws  Exception   In case of parse error
     */
    public long parseTimeDiff(String str) throws Exception{
        long time = 0;
        if (str == null) return 0;

        str = StringParser.beautifyString(str);
        String[] stringArray = str.split("\\s+");
        boolean isNumber = true;
        int number = 0;

        //Go through all the tokens of the timediff expression
        for (int i = 0; i < stringArray.length; ++i){
            //If we expect a number, try to parse it
            if (isNumber) { 
                try {
                    number = Integer.decode(stringArray[i]).intValue();
                } catch (Exception e) {
                    if (stringArray[i].equals("")) stringArray[i] = "nothing";
                    throw new Exception("Number expected, but "+stringArray[i]+" found.");
                }
            //Else multiply the previous number with the corresponding number of milliseconds
            // (e.g.: 2 hours -> 2 * 3600000)
            } else {
                int j;
                for (j = 0; j < timeDiffNum; ++j){
                    if (timeDiffStrings[j].equals(stringArray[i])) {
                        time += number * timeDiffValues[j];
                        break;
                    }
                }
                //Unknown time difference specifier
                if (j == timeDiffNum) throw new Exception("Unknown time difference specifier: "+stringArray[i]);
                number = 0;
            }
            isNumber = !isNumber;
        }
        if (!isNumber) throw new Exception("Time difference specifier expected after "+number);

        return time;
    }

    /**
     * Parse the given string as a time origin expression.
     * The time has to be expressed in local time (client time), so that relative time specifiers
     * such as now or tomorrow work correctly
     *
     * @param	str	The string to be parsed
     * @param	offset	The user time offset
     * @return 		The representation of the time origin in milliseconds
     * @throws  Exception   In case of parse error
     */
    private long parseTime(String str, long offset) throws Exception{
        long time = offset;
        if (str == null) return 0;
        int a = str.indexOf('/');
        String timeStr = null;
        String day = null;

        cal = Calendar.getInstance();

        //If there's a '/' character, parse the string before it as a date
        if (a != -1) {
            day = str.substring(0,a);
            if (a != str.length())
                timeStr = str.substring(a+1, str.length());

            //count the number of dots in the pattern (one or two)
            int dotPosition = day.indexOf('.');
            dotPosition = day.indexOf('.', dotPosition+1);
            try {
                SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy");
                //if there are two or more dots, parse the day as dd.MM.yyyy
                if (dotPosition != -1) {
                    time += sdf.parse(day).getTime();
                //if there are less than two dots, parse day as dd.MM
                } else {
                    time += sdf.parse(day+"."+cal.get(Calendar.YEAR)).getTime();
                }
            } catch (ParseException e) {
                throw new ParseException("Could not parse the date before '/' character", 0);
            }
        //Else set the date to today
        } else {
            timeStr = str;

            //Check, whether the expressions are not literals
            if (str.equals("now")){
                //return actual time
                return time + cal.getTimeInMillis();
            } else if (str.equals("tomorrow")){
                //now + 24*60*60*1000 milliseconds (one day)
                return time + cal.getTimeInMillis() + 86400000;
            } else if (str.equals("next week")){
                //now + 7*24*60*60*1000 milliseconds (seven days)
                return time + cal.getTimeInMillis() + 604800000;
            } else {
                //The time will contain the local time of the client now
                time += cal.getTimeInMillis();
                //Truncate the value in time down to whole days
                time = time / timeDiffValues[3] * timeDiffValues[3];
            }
        }

        //Now we have the date, let's parse the time!
        if (timeStr == null) throw new ParseException("Missing time after the '/' character", a);

        //count the number of double dots
        int ddPosition = timeStr.indexOf(':');
        ddPosition = timeStr.indexOf(':', ddPosition+1);

        try {
            //If we have two or more double dots, parse the time as HH:mm:ss
            if (ddPosition != -1) {
                SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                time += sdf.parse(timeStr).getTime();
            //else, parse the time as MM:mm
            } else {
                SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");
                time += sdf.parse(timeStr).getTime();
            }
        } catch (Exception e) {
            throw new ParseException("Could not parse the time after '/' character", a);
        }

        //We should have the whole time origin expression parsed now, return the time
        return time;
    }

    //Date formats used when parsing
    private SimpleDateFormat sqlTimeFormat = new SimpleDateFormat("yyMMddHHmmss");
    private SimpleDateFormat readableTimeFormat =  new SimpleDateFormat("dd.MM.yyyy/HH:mm:ss");

    //Helper calendar variables
    private Calendar cal = null;
    private Calendar remoteCal = null;

    //Strings representing different time intervals, used in timediffs
    private int timeDiffNum = 5;
    private String[] timeDiffStrings = {"seconds", "minutes", "hours", "days", "weeks"};
    private long[] timeDiffValues = {1000, 60000, 3600000, 86400000, 604800000};
}
